package org.senf.outerspace.galaxy.view;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.media.opengl.GL;
import javax.media.opengl.GLContext;
import javax.media.opengl.GLDrawableFactory;
import javax.media.opengl.glu.GLU;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.opengl.GLCanvas;
import org.eclipse.swt.opengl.GLData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.ViewPart;
import org.osgi.framework.Bundle;
import org.senf.outerspace.galaxy.Activator;
import org.senf.outerspace.galaxy.Galaxy;
import org.senf.outerspace.galaxy.GameClock;
import org.senf.outerspace.galaxy.ShipPosition;
import org.senf.outerspace.ships.build.Ship;
import org.senf.outerspace.ships.event.ShipEvent;
import org.senf.outerspace.ships.event.ShipEventListener;
import org.senf.outerspace.ships.ui.ShipDesignView;

import com.sun.opengl.util.GLUT;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureIO;

public class GalaxyView extends ViewPart implements MouseListener, MouseMoveListener, MouseWheelListener, KeyListener,
	ShipEventListener {

	public final static String ID = "org.senf.outerspace.galaxy.view.GalaxyView";

	private Composite container;
	private GLCanvas canvas;
	private GLContext context;

	private GL gl;
	private GLU glu;
	private GLUT glut;
	
	private Texture texture;
	
	private int cellSizeX = 20;
	private int cellSizeY = 20;
	
	private int currentX = 0;
	private int currentY = 0;
	private float zoomFactor = 1.0f;
	private float minZoom = 0.5f;
	private float maxZoom = 10.0f;
	
	private boolean dragging;
	private Point dragStart;
	private Point dragPoint;
	
	private List<Point> suns;
	private int selectedX;
	private int selectedY;
	
	private int viewPortMtx[] = new int[4];
	private double modelViewMtx[] = new double[16];
	private double projectionMtx[] = new double[16];
	
	@Override
	public void createPartControl(Composite parent) {
		parent.setLayout(new FillLayout());
		
		container = new Composite(parent, SWT.NONE);
		container.setLayout(new FillLayout());

		GLData data = new GLData ();
		data.doubleBuffer = true;

		canvas = new GLCanvas(container, SWT.NONE, data);
		canvas.setCurrent();
		
		context = GLDrawableFactory.getFactory().createExternalGLContext();
		context.makeCurrent();
		
		gl = context.getGL();
		glu = new GLU();
		glut = new GLUT();
		
		gl.glEnable(GL.GL_DEPTH_TEST);
		
		loadBgTexture();
				
		suns = new ArrayList<Point>();
		for (int i=0; i<5; i++) {
			int x = (int)(Math.random()*Galaxy.getInstance().getSizeX());
			int y = (int)(Math.random()*Galaxy.getInstance().getSizeY());
			Point p = new Point(x,y);
			suns.add(p);
		}

		canvas.addMouseListener(this);
		canvas.addMouseMoveListener(this);
		container.addMouseWheelListener(this);
		container.addKeyListener(this);
		
		// Make sure an initial render is done.
		Display.getCurrent().asyncExec(new Runnable(){
			@Override
			public void run() {
				GameClock.getInstance().start();
			}
		});
		
		// Listen to ship design events.
		ShipDesignView designView = (ShipDesignView)PlatformUI.getWorkbench()
			.getActiveWorkbenchWindow().getActivePage().findView(ShipDesignView.ID);
		designView.addListener(this);
	}

	@Override
	public void setFocus() {
		container.setFocus();
	}
	
	private void loadBgTexture() {
		InputStream in = null;
		try {
			Bundle b = Activator.getDefault().getBundle();
			IPath path = new Path("/images/stars.bmp");
			URL url = FileLocator.find(b, path, null);
			in = url.openConnection().getInputStream();
			texture = TextureIO.newTexture(in, false, null);
			texture.setTexParameteri(GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
			texture.setTexParameteri(GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
		} catch (IOException e) {
			if (in != null) {
				try { in.close(); } catch (Exception e2) {}
			}
			throw new RuntimeException(e);
		}
	}

	private void setPerspective() {
		Rectangle bounds = canvas.getBounds();
		float fAspect = (float) bounds.width / (float) bounds.height;
		
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		gl.glViewport(0, 0, bounds.width, bounds.height);
		glu.gluPerspective(50.0f, fAspect, 1f, 1100.0f);
		
		gl.glGetIntegerv(GL.GL_VIEWPORT, viewPortMtx, 0);
        gl.glGetDoublev(GL.GL_PROJECTION_MATRIX, projectionMtx, 0);
	}
	
	private void setOrtho() {
		Rectangle bounds = canvas.getBounds();
		float halfWidth = ((float)bounds.width)/2;
		float halfHeight = ((float)bounds.height)/2;
		
		gl.glDisable(GL.GL_DEPTH_TEST);
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		gl.glOrtho(-halfWidth, halfWidth, -halfHeight, halfHeight, 1f, 1100f);
	}
	
	private void calculateSelectionBox(int winX, int winY) {
        gl.glGetDoublev(GL.GL_MODELVIEW_MATRIX, modelViewMtx, 0);
        winY = viewPortMtx[3] - winY - 1;

	    double near[] = new double[4];
        glu.gluUnProject(winX, winY, 0f, modelViewMtx, 0, projectionMtx, 0, viewPortMtx, 0, near, 0);
        double far[] = new double[4];
        glu.gluUnProject(winX, winY, 1f, modelViewMtx, 0, projectionMtx, 0, viewPortMtx, 0, far, 0);

        // Solve x,y for u.
        double distance = Math.abs(far[2]-near[2]);
        double u = near[2]/distance;
        double x = near[0] + u*(far[0]-near[0]);
        double y = near[1] + u*(far[1]-near[1]);
        
        selectedX = ((int)x/cellSizeX)*cellSizeX;
        selectedY = ((int)y/cellSizeY)*cellSizeY;
        
//        drawScene();
	}
	
	/*
	 * ******************
	 * Drawing operations
	 * ******************
	 */
		
	public void drawScene() {
		if (!canvas.isDisposed()) {
			gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
			
			setOrtho();
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glLoadIdentity();
			drawBackground();
			
			setPerspective();
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glLoadIdentity();
			glu.gluLookAt(0.0+currentX, 0.0+currentY, 100.0*zoomFactor,
					0.0+currentX, 0.0+currentY, 0.0, 0.0, 1.0, 0.0);
			
			drawGrid();
			drawSuns();
			drawShips();
			drawSelectionBox();
			canvas.swapBuffers();
		}
	}
	
	private void drawBackground() {
		Rectangle bounds = canvas.getBounds();
		float halfWidth = ((float)bounds.width)/2;
		float halfHeight = ((float)bounds.height)/2;
		
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl.glColor3f(1f,1f,1f);
		gl.glEnable(GL.GL_TEXTURE_2D);
		texture.bind();
		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2f(0.0f, 1.0f);
		gl.glVertex3f(-halfWidth, -halfHeight, -1.0f);
		gl.glTexCoord2f(1.0f, 1.0f);
		gl.glVertex3f(halfWidth, -halfHeight, -1.0f);
		gl.glTexCoord2f(1.0f, 0.0f);
		gl.glVertex3f(halfWidth, halfHeight, -1.0f);
		gl.glTexCoord2f(0.0f, 0.0f);
		gl.glVertex3f(-halfWidth, halfHeight, -1.0f);
		gl.glEnd();
		gl.glDisable(GL.GL_TEXTURE_2D);
	}

	private void drawGrid() {
		int sizeX = Galaxy.getInstance().getSizeX();
		int sizeY = Galaxy.getInstance().getSizeY();
		gl.glLineWidth(1f);
		gl.glColor3f(0f,0f,1f);
		gl.glBegin(GL.GL_LINES);
		for (int x=0; x<=sizeX; x++) {
			gl.glVertex3f(x*cellSizeX,0f,0f);
			gl.glVertex3f(x*cellSizeX,sizeY*cellSizeY,0f);
		}
		for (int y=0; y<=sizeY; y++) {
			gl.glVertex3f(0f,y*cellSizeY,0f);
			gl.glVertex3f(sizeX*cellSizeX,y*cellSizeY,0f);
		}
		gl.glEnd();
	}
	
	private void drawSuns() {
		gl.glLineWidth(1f);
		gl.glColor3f(0f,1f,0f);
		for (Point sun: suns) {
			gl.glPushMatrix();
			gl.glTranslatef((cellSizeX/2)+(cellSizeX*sun.x), (cellSizeX/2)+(cellSizeY*sun.y), 0);
			glut.glutWireSphere(8, 15, 15);
			gl.glPopMatrix();
		}
	}
	
	private void drawShips() {
		Galaxy g = Galaxy.getInstance();
		List<Ship> ships = g.getShips();
		for (Ship ship: ships) {
			gl.glPushMatrix();
			ShipPosition pos = g.getPosition(ship);
			int cellX = (int)(pos.x/cellSizeX);
			int cellY = (int)(pos.y/cellSizeY);
			gl.glTranslatef((cellSizeX/2)+(cellSizeX*cellX), (cellSizeX/2)+(cellSizeY*cellY), 0);
//			gl.glTranslatef((cellSizeX/2)+pos.x, (cellSizeX/2)+pos.y, 0);
			ship.getModel().getBluePrint().render(gl);
			gl.glPopMatrix();
		}
	}
	
	private void drawSelectionBox() {
		int x = selectedX;
		int y = selectedY;
		gl.glLineWidth(4f);
		gl.glColor3f(0.3f,1f,1f);
		gl.glBegin(GL.GL_LINE_LOOP);
		gl.glVertex3f(x,y,0f);
		gl.glVertex3f(x+cellSizeX,y,0f);
		gl.glVertex3f(x+cellSizeX,y+cellSizeY,0f);
		gl.glVertex3f(x,y+cellSizeY,0f);
		gl.glEnd();
	}
	
	/*
	 * ********************
	 * Mouse and key events
	 * ********************
	 */
	
	@Override
	public void keyPressed(KeyEvent e) {
		if (e.keyCode == SWT.ARROW_UP) {
			currentY += 10;
		} else if (e.keyCode == SWT.ARROW_DOWN) {
			currentY -= 10;
		} else if (e.keyCode == SWT.ARROW_LEFT) {
			currentX -= 10;	
		} else if (e.keyCode == SWT.ARROW_RIGHT) {
			currentX += 10;
		}
	}

	@Override
	public void keyReleased(KeyEvent e) {
		// Do nothing.
	}
	
	@Override
	public void mouseScrolled(MouseEvent e) {
		int c = e.count;
		if (c < 0) {
			// Zoom out
			zoomFactor += 0.1f;
			if (zoomFactor > maxZoom) zoomFactor = maxZoom;
		} else {
			// Zoom in
			zoomFactor -= 0.1f;
			if (zoomFactor < minZoom) zoomFactor = minZoom;
		}
	}

	@Override
	public void mouseMove(MouseEvent e) {
		if (dragging) {
			int diffX = e.x - dragPoint.x;
			int diffY = e.y - dragPoint.y;
			//TODO: Slower zooming on zoomin, faster on zoomout.
			double factor = 0.4;
			int relativeDiffX = (int)(factor*diffX);
			int relativeDiffY = (int)(factor*diffY);
			if (diffX != 0 && relativeDiffX == 0) relativeDiffX = diffX/Math.abs(diffX);
			if (diffY != 0 && relativeDiffY == 0) relativeDiffY = diffY/Math.abs(diffY);
			currentX -= relativeDiffX;
			currentY += relativeDiffY;
			dragPoint = new Point(e.x,e.y);
		}
	}

	@Override
	public void mouseDoubleClick(MouseEvent e) {
		// Do nothing.
	}

	@Override
	public void mouseDown(MouseEvent e) {
		dragging = true;
		dragPoint = new Point(e.x, e.y);
		dragStart = new Point(e.x, e.y);
	}

	@Override
	public void mouseUp(MouseEvent e) {
		dragging = false;

		int xDiff = Math.abs(e.x-dragStart.x);
		int yDiff = Math.abs(e.y-dragStart.y);
		if (xDiff < 2 && yDiff < 2) {
			// Treat as a selection click.
			calculateSelectionBox(e.x, e.y);
		}
	}

	/*
	 * ***********
	 * Ship Events
	 * ***********
	 */
	
	@Override
	public void handle(ShipEvent e) {
		if (e.type == ShipEvent.TYPE_SHIP_CREATED) {
			Galaxy g = Galaxy.getInstance();
			Ship ship = (Ship)e.data;
			g.addShip(ship);
			
			int sizeX = Galaxy.getInstance().getSizeX();
			int sizeY = Galaxy.getInstance().getSizeY();
			int x = (int)(Math.random()*sizeX*cellSizeX);
			int y = (int)(Math.random()*sizeY*cellSizeY);	
			ShipPosition p = new ShipPosition(x,y);
			g.setPosition(ship, p);
		}
	}
}
